Skip to content

Port os_hardening to Rust, and let a kernel-only capture answer - #24

Merged
Zenofex merged 1 commit into
mainfrom
feat/os-hardening
Sep 28, 2026
Merged

Zenofex merged 1 commit into
mainfrom
feat/os-hardening

Conversation

@Zenofex

@Zenofex Zenofex commented Sep 28, 2026

Copy link
Copy Markdown
Contributor

The engine reads the kernel's hardening report (BootIntel.com@5508944, expectation at 0a426e1e). Until the CLI does too, an operator working offline gets "no U-Boot session found; nothing was assessed" on a capture that plainly states its KASLR is off because the bootloader supplied no seed.

no U-Boot session in kaslr-no-seed.log
  kernel hardening
    memory init  stack:off  heap alloc:off  heap free:off
    KASLR        disabled (lack of seed)

Three kernel-stage fixtures, all real captures

Fixture Source What it pins
selinux-ignored.log bootintel-6 selinux=0 under Unknown command line parameters: — the kernel ignored it, so SELinux is not compiled in rather than switched off
kernel-hardening.log bootintel-21 KASLR on, memory init off, LSM list of capability,integrity with no mandatory access control
kaslr-no-seed.log bootintel-27 KASLR off because the bootloader supplied no entropy

Both implementations reproduce expect.txt byte for byte across eight fixtures now, five of them real captures.

The distinctions that decide whether the output is trustworthy

  • selinux=0 means opposite things depending on the line it sits on. Reporting the ignored-parameter case as "disabled by boot parameter" would describe a device that does not exist, while understating a worse fact.
  • capability in the LSM list is not access control. lsm=capability,integrity reports no MAC; lsm=capability,yama,apparmor correctly reports AppArmor.
  • mac_modules renders even when empty, because "an LSM line was seen and none provide MAC" is a different claim from "no LSM line was seen". A renderer collapsing the two would hide exactly the divergence the expectation exists to catch.
  • Absence is never evidence, and it is tested: a capture that never mentions KASLR is not a capture proving it off.

Behaviour change worth reading

verdict used to exit 3 whenever there was no U-Boot session. That was right when the session was the only thing it assessed, and became wrong the moment a plain boot log could produce a real answer — "nothing was assessed" would be false, and a CI job keyed on that code would treat an answer as a failure to answer.

It now exits 3 only when the capture yields neither a session nor a posture. A capture with neither is tested.

No findings are raised on the Rust side, deliberately: the detector sets are pinned at 14 labels across Rust and the browser library, and a fifteenth would break that parity. The engine raises the findings; both sides share the facts.

Verification

  • cargo test --workspace: 355 passed, 0 failed
  • clippy --workspace --all-targets -- -D warnings and fmt --all --check: clean
  • Engine side: 17 passed across the parity and hardening suites, pushed
  • Ran the built binary against all three new fixtures

🤖 Generated with Claude Code

The engine reads the kernel's hardening report; until the CLI does too, an
operator working offline gets "no U-Boot session found; nothing was assessed" on
a capture that plainly states its KASLR is off because the bootloader supplied no
seed.

Three kernel-stage fixtures now sit in the shared expectation, so the two
implementations cannot drift on this the way they could have on boot_integrity:

  selinux-ignored.log    bootintel-6 lines 145-155. The trap: `selinux=0` listed
                         under `Unknown command line parameters:`, which means
                         the kernel IGNORED it and SELinux is not compiled in,
                         rather than switched off.
  kernel-hardening.log   bootintel-21 lines 38-115. KASLR on, memory init off,
                         and an LSM list of `capability,integrity` that contains
                         no mandatory access control at all.
  kaslr-no-seed.log      bootintel-27 lines 148-234. The embedded failure mode,
                         where the kernel wanted to randomise and the bootloader
                         handed it no entropy.

Both sides reproduce the expectation byte for byte across eight fixtures now,
five of them real captures.

Behaviour change worth reading: `verdict` used to exit 3 whenever there was no
U-Boot session. That was right when the session was the only thing it assessed
and became wrong the moment a plain boot log could produce a real answer.
Exiting 3 with "nothing was assessed" on a capture that just reported its
hardening posture is false, and a CI job keyed on that code would treat an answer
as a failure to answer. It now exits 3 only when the capture yields neither, and
a capture with neither is tested.

No findings are raised on the Rust side, deliberately: the detector sets are
pinned at 14 labels across Rust and the browser library, and a fifteenth would
break that parity. The engine raises the findings; both sides share the facts.

355 tests, clippy clean under -D warnings, rustfmt clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Zenofex
Zenofex merged commit 99a52ff into main Sep 28, 2026
11 checks passed
@Zenofex
Zenofex deleted the feat/os-hardening branch September 28, 2026 21:10
Zenofex added a commit that referenced this pull request Sep 28, 2026
Version bump, lockfile, changelog. No code changes: everything here is
already on `main` and was reviewed in #24.

## What ships

`bootintel verdict` answers for captures that never reach a U-Boot
prompt:

```
no U-Boot session in boot.log
  kernel hardening
    memory init  stack:off  heap alloc:off  heap free:off
    KASLR        disabled (lack of seed)
    LSM          capability, integrity  (none provide mandatory access control)
```

Mandatory access control, memory initialisation and kernel address
randomisation, read from what the kernel itself announced. Ported from
the engine and pinned against it by three kernel-stage fixtures.

## Why MINOR

Either reason alone is enough:

- New behaviour.
- **Changed contract.** `verdict` used to exit 3 whenever there was no
U-Boot session; it now exits 3 only when a capture yields neither a
session nor a hardening posture. The old behaviour became wrong the
moment a plain boot log could produce a real answer — a CI job keyed on
that code would have treated an answer as a failure to answer. A capture
with neither still exits 3, and that case is tested.

## Verification

- `cargo test --workspace`: 364 passed, 0 failed
- `clippy --workspace --all-targets -- -D warnings` and `fmt --all
--check`: clean
- `cargo build --release` then `bootintel --version` reports `bootintel
0.10.0`
- Both implementations reproduce the shared expectation byte for byte
across eight fixtures, five of them real captures

Both version bumps landed first try — third release running for
`docs/releasing.md` step 1, which exists because the
`bootintel-detectors` pin is not derived by cargo.

After merge: dispatch `cli-release` for `0.10.0` with `publish_crates`,
verify the draft against `SHA256SUMS`, publish and mark latest, then
move the tap (step 7) and sync the in-repo reference copy.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant